package jet

import (
	"errors"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"reflect"
	"strings"
	"sync"
	"text/template"

	. "gitverse.ru/Ao/Logger"
)

// Set is responsible to load, parse and cache templates.
// Every Jet template is associated with a Set.
// Set отвечает за загрузку, синтаксический анализ и кэширование шаблонов.
// Каждый шаблон Jet связан с набором.
type Set struct {
	loader            Loader
	cache             Cache // Кэш шаблонов, тут хранятся шаблоны не блоки
	кэшБлоков         Cache
	JsБлоки           Cache
	escapee           SafeWriter    // escapee to use at runtime
	globals           VarMap        // global scope for this template set
	gmx               *sync.RWMutex // global variables map mutex
	extensions        []string
	режимРазработчика bool
	leftDelim         string
	rightDelim        string
	leftComment       string
	rightComment      string
}

// Option is the type of option functions that can be used in NewSet().
type Option func(*Set)
type КартаФункций map[string]any

// NewSet returns a new Set relying on loader. NewSet panics if a nil Loader is passed.
func NewSet(loader Loader, opts ...Option) *Set {
	if loader == nil {
		panic(errors.New("jet: NewSet() must not be called with a nil loader"))
	}

	s := &Set{
		loader:     loader,
		cache:      &cache{},
		кэшБлоков:  &cache{},
		JsБлоки:    &cache{},
		escapee:    template.HTMLEscape,
		globals:    VarMap{},
		gmx:        &sync.RWMutex{},
		extensions: []string{
			// "", // in case the path is given with the correct extension already
			// ".html", // in case the path is given with the correct extension already
			// ".js",   // in case the path is given with the correct extension already
			// ".jet",
			// ".html.jet",
			// ".jet.html",
		},
	}

	for _, opt := range opts {
		opt(s)
	}

	return s
}

// WithCache returns an option function that sets the cache to use for template parsing results.
// Use InDevelopmentMode() to disable caching of parsed templates. By default, Jet uses a
// concurrency-safe in-memory cache that holds templates forever.
// // With Cache возвращает функцию option, которая настраивает использование кэша для результатов анализа шаблонов.
// // Используйте в режиме разработки(), чтобы отключить кэширование проанализированных шаблонов. По умолчанию Jet использует защищенный от параллелизма кэш в памяти, в котором шаблоны хранятся вечно
func WithCache(c Cache) Option {
	if c == nil {
		panic(errors.New("jet: WithCache() must not be called with a nil cache"))
	}
	return func(s *Set) {
		s.cache = c
	}
}

// WithSafeWriter returns an option function that sets the escaping function to use when executing
// templates. By default, Jet uses a writer that takes care of HTML escaping. Pass nil to disable escaping.
func WithSafeWriter(w SafeWriter) Option {
	return func(s *Set) {
		s.escapee = w
	}
}

// WithDelims returns an option function that sets the delimiters to the specified strings.
// Parsed templates will inherit the settings. Not setting them leaves them at the default: `{{` and `}}`.
func WithDelims(left, right string) Option {
	return func(s *Set) {
		s.leftDelim = left
		s.rightDelim = right
	}
}

// WithCommentDelims returns an option function that sets the comment delimiters to the specified strings.
// Parsed templates will inherit the settings. Not setting them leaves them at the default: `{*` and `*}`.
func WithCommentDelims(left, right string) Option {
	return func(s *Set) {
		s.leftComment = left
		s.rightComment = right
	}
}

// WithTemplateNameExtensions returns an option function that sets the extensions to try when looking
// up template names in the cache or loader. Default extensions are `""` (no extension), `".jet"`,
// `".html.jet"`, `".jet.html"`. Extensions will be tried in the order they are defined in the slice.
// WithTemplateNameExtensions panics when you pass in a nil or empty slice.
func WithTemplateNameExtensions(extensions []string) Option {
	if len(extensions) == 0 {
		panic(errors.New("jet: WithTemplateNameExtensions() must not be called with a nil or empty slice of extensions"))
	}
	return func(s *Set) {
		s.extensions = extensions
	}
}

// // WРасширенияШаблонов возвращает функцию option, которая устанавливает, какие расширения следует использовать при поиске
// по именам шаблонов в кэше или загрузчике. Расширениями по умолчанию являются """" (без расширения), "".jet"",
// "".html.jet"", "".jet.html "`. Расширения будут опробованы в том порядке, в котором они определены в срезе.
// WithTemplateNameExtensions вызывает панику, когда вы вводите нулевой или пустой фрагмент.
// расширение добавляеться к переданому в функцию ПарсингШаблона имени ф
func РасширенияШаблонов(extensions []string) Option {
	if len(extensions) == 0 {
		panic(errors.New("jet: РасширенияШаблонов() must not be called with a nil or empty slice of extensions"))
	}
	return func(s *Set) {
		s.extensions = extensions
	}
}

// InDevelopmentMode returns an option function that toggles development mode on, meaning the cache will
// always be bypassed and every template lookup will go to the loader.
func InDevelopmentMode() Option {
	return DevelopmentMode(true)
}

func РежимРазработки() Option {
	return func(s *Set) {
		s.режимРазработчика = true
	}
}

// DevelopmentMode returns an option function that sets development mode on or off. "On" means the cache will
// always be bypassed and every template lookup will go to the loader.
func DevelopmentMode(mode bool) Option {
	return func(s *Set) {
		s.режимРазработчика = mode
	}
}

func (набор *Set) ПарсингДиректории(директорияШаблонов string) {
	ошибка := filepath.WalkDir(директорияШаблонов, func(путь string, описание os.DirEntry, err error) error {

		if описание.IsDir() {
			// fi, ошибка := описание.Info()
			// if ошибка != nil {
			// 	Ошибка(" ОписаниеОшибки  %+v \n", ошибка)
			// }
			return nil
		}
		_, ошибка := набор.ПарсингШаблона(путь, false)
		if ошибка != nil {
			Ошибка(" Ошибка парсинга  %+v \n", ошибка.Error())
		}

		return nil
	})

	if ошибка != nil {
		Ошибка(" Ошибка обработки дирректории   %+v \n", ошибка)
	}
}

func (набор *Set) ПарсингДиректорииШаблонов() {
	// Инфо(" ПарсингДиректорииШаблонов %+v \n", набор.loader.Dir())

	ошибка := filepath.WalkDir(набор.loader.Dir(), func(путь string, описание os.DirEntry, err error) error {

		if описание.IsDir() {
			// fi, ошибка := описание.Info()
			// if ошибка != nil {
			// 	Ошибка(" ОписаниеОшибки  %+v \n", ошибка)
			// }
			return nil
		}
		// Инфо(" описание %+v \n", описание.Name())

		// относительныйПутьКФайлу := путь[len(набор.loader.Dir()):]
		// Инфо("относительныйПутьКФайлу %+v \n", относительныйПутьКФайлу)

		_, ошибка := набор.ПарсингШаблона(путь[len(набор.loader.Dir()):], false)
		if ошибка != nil {

			Ошибка(" Ошибка парсинга  %+v \n", ошибка.Error())
			// panic(ошибка)
		}

		return nil
	})

	if ошибка != nil {
		Ошибка(" Ошибка обработки дирректории   %+v \n", ошибка)
	}
}

// GetTemplate tries to find (and parse, if not yet parsed) the template at the specified path.
//
// For example, GetTemplate("catalog/products.list") with extensions set to []string{"", ".html.jet",".jet"}
// will try to look for:
//  1. catalog/products.list
//  2. catalog/products.list.html.jet
//  3. catalog/products.list.jet
//
// in the set's templates cache, and if it can't find the template it will try to load the same paths via
// the loader, and, if parsed successfully, cache the template (unless running in development mode).
func (s *Set) GetTemplate(templatePath string) (t *Template, err error) {
	// Инфо(" GetTemplate  кэшировать %+v \n", false)

	return s.getSiblingTemplate(templatePath, "/", true)
}

// ПарсингШаблона пытается найти (и проанализировать, если еще не проанализирован) шаблон по указанному пути.
//
// Например, ПарсингШаблона("каталог/products.list") с расширениями, установленными в []строку{"", ".html.jet",".jet"}
// попытается найти:
// 1. каталог/products.list
// 2. catalog/products.list.html.jet
// 3. catalog/products.list.jet
//
// в кэше шаблонов набора, и если он не сможет найти шаблон, он попытается загрузить те же пути через
// загрузчик и, в случае успешного анализа, кэширует шаблон (если только он не запущен в режиме разработки).
func (s *Set) ПарсингШаблона(templatePath string, получитьИзКэша bool) (t *Template, err error) {
	Инфо("ПарсингШаблона templatePath %+v \n", templatePath)

	return s.getSiblingTemplate(templatePath, "/", получитьИзКэша)
}

func (s *Set) ПарсингШаблонаСДиска(путьКонтентШаблона string) (t *Template, err error) {
	Инфо("ПарсингШаблона templatePath %+v \n", путьКонтентШаблона)

	t, err = s.getTemplateFromLoader(путьКонтентШаблона, true)
	if err == nil { //&& (кэшироватьПослеПарсинга == true || s.режимРазработчика == false)
		// Инфо(" Кэшируем набор шаблонов, кэшироватьПослеПарсинга %+v \n", templatePath)

		s.cache.Put(путьКонтентШаблона, t)
	}

	return t, err

}

func (s *Set) getSiblingTemplate(templatePath, siblingPath string, получитьИзКэша bool) (t *Template, err error) {
	templatePath = filepath.ToSlash(templatePath)
	siblingPath = filepath.ToSlash(siblingPath)
	// Инфо("getSiblingTemplate %+v \n", siblingPath)

	if !path.IsAbs(templatePath) { // проверят что путь начинается с /
		siblingDir := path.Dir(siblingPath)
		templatePath = path.Join(siblingDir, templatePath)
	}

	// проверим что путь окначиваеться на html
	// if !strings.HasSuffix(templatePath, ".html") {
	templatePath = проверитьРасширениеФайла(templatePath)
	// }

	return s.getTemplate(templatePath, получитьИзКэша)
}

func проверитьРасширениеФайла(путьКФайлу string) string {
	расширениеФайла := filepath.Ext(путьКФайлу)
	if расширениеФайла == "" {
		return путьКФайлу + ".html"
	} else if расширениеФайла != ".html" {
		// оставляем как есть, не изменяем расширение
		return путьКФайлу
	}
	return путьКФайлу
}

// same as GetTemplate, but doesn't cache a template when found through the loader.
// Возвращает найденный шаблон, если он найден в кэше, иначе загружает шаблон из файловой системы, апрсит его.
func (s *Set) getTemplate(templatePath string, получитьИзКэша bool) (*Template, error) {
	// Инфо(" templatePath %+v \n", templatePath)
	// если кэшироватьПослеПарсинга = true то не ищем шаблонв в кэше, его нужно обновить, поэтому
	//продолжаем минуя кэш
	// Инфо("кэшироватьПослеПарсинга %+v \n", кэшироватьПослеПарсинга)
	var err error
	var t *Template
	var найденВКэше bool
	найденВКэше = false
	Инфо(" получитьИзКэша %+v \n", получитьИзКэша)

	if получитьИзКэша {
		t, найденВКэше = s.getTemplateFromCache(templatePath)

	}

	if найденВКэше {
		Инфо(" Шаблон найден в кэше %+v \n", templatePath)
	} else {
		Инфо("шаблон %+v не найден в кэше , открываем с диска \n", templatePath)
		t, err = s.getTemplateFromLoader(templatePath, получитьИзКэша)
		if err == nil {
			s.cache.Put(templatePath, t)
		}
	}

	// t, err = s.getTemplateFromLoader(templatePath, кэшироватьПослеПарсинга)
	// Ошибка("!Внимаение: Берём из ФАЙЛА кэшироватьПослеПарсинга = %+v \n", кэшироватьПослеПарсинга)

	// if err == nil { //&& (кэшироватьПослеПарсинга == true || s.режимРазработчика == false)
	// 	// Инфо(" Кэшируем набор шаблонов, кэшироватьПослеПарсинга %+v \n", templatePath)

	// 	s.cache.Put(templatePath, t)
	// }
	if err != nil {
		Ошибка(" s.getTemplateFromLoader %+v \n", err.Error())

	}
	return t, err

}

func (s *Set) ДобавитьДеревоШаблона(имяШаблона string, t *Template) {
	s.cache.Put(имяШаблона, t)
}

// ДобавитьБлок - добавляет блоки глобальный кэш набора, сортируя блоки с js кодом, и обычные.
func (s *Set) ДобавитьБлок(имяБлока string, б *BlockNode) {

	if strings.HasSuffix(б.Name, "_js") {
		s.JsБлоки.ДобавитьБлок(имяБлока, б)
	} else {
		s.кэшБлоков.ДобавитьБлок(имяБлока, б)
	}
}

func (s *Set) НайтиБлок(имяБлока string) *BlockNode {
	var блок *BlockNode
	if strings.HasSuffix(имяБлока, "_js") {
		блок = s.JsБлоки.НайтиБлок(имяБлока)
	} else {
		блок = s.кэшБлоков.НайтиБлок(имяБлока)
	}
	return блок
}

/*
Возвращает найденный блок, который добавлен в виртуальный Контент блок
путьКонтентШаблона = может быть не просто именем а содержать папки , "вложенныйКонтент/"+картаМаршрута[0],

имяКонтентБлока может быть или Контент, или имяОсновногоШаблона_контент
*/
func (s *Set) ДобавитьКонтентБлок(каталогШаблоновРоли string, путьКонтентШаблона string, имяКонтентБлока string) (*Template, error) {

	путьШаблона := "/контент/" + каталогШаблоновРоли + "/" + путьКонтентШаблона + ".html"

	Инфо("имяШаблона %+v имяКонтентШаблона %+v \n", путьШаблона, путьКонтентШаблона)

	// создаем новый блок Контент и вставляем в него путь к  шаблону, и отправялем парситься, в итоге в кэше блоков появится
	шаблонКонтент, ошибка := s.parse(путьШаблона, `{{блок `+имяКонтентБлока+`()}}{{шаблон "`+путьШаблона+`"}}{{конец}}`, true)

	if ошибка != nil {
		Ошибка(" Ошибка парсинга шаблона из строки %+s  %+v \n", `{{блок `+имяКонтентБлока+`()}}{{шаблон "`+путьШаблона+`"}}{{конец}}`, ошибка.Error())

		return nil, fmt.Errorf("ошибка парсинга шаблона из строки %+s  %+v ", `{{блок `+имяКонтентБлока+`()}}{{шаблон "`+путьШаблона+`"}}{{конец}}`, ошибка.Error())
	}
	// s.ДобавитьБлок("Контент", б)

	return шаблонКонтент, nil

}

func (s *Set) getTemplateFromCache(templatePath string) (t *Template, ok bool) {
	// check path with all possible extensions in cache
	// for _, extension := range s.extensions {
	// canonicalPath := templatePath + extension
	// if t := s.cache.Get(canonicalPath); t != nil {
	if t := s.cache.Get(templatePath); t != nil {
		return t, true
	}
	// }
	return nil, false
}

func (s *Set) getTemplateFromLoader(templatePath string, cacheAfterParsing bool) (t *Template, err error) {
	// check path with all possible extensions in loader

	// for _, extension := range s.extensions {
	// canonicalPath := templatePath + extension
	// 	Инфо("canonicalPath %+v \n", canonicalPath)

	if found := s.loader.Exists(templatePath); found {
		return s.loadFromFile(templatePath, cacheAfterParsing)
	}
	// }

	return nil, fmt.Errorf("(getTemplateFromLoader)шаблон  '%s' не найден", templatePath)
}

func (s *Set) loadFromFile(templatePath string, cacheAfterParsing bool) (template *Template, err error) {
	// Инфо("loadFromFile %+v \n", templatePath)
	f, err := s.loader.Open(templatePath)
	// Инфо("loadFromFile %+v \n", f)
	if err != nil {
		return nil, err
	}
	defer f.Close()
	content, err := io.ReadAll(f)
	// Инфо(" %+v \n", content)
	if err != nil {
		return nil, err
	}
	return s.parse(templatePath, string(content), cacheAfterParsing)
}
func (s *Set) ПоказатьШаблоныйВКэше() {
	// Инфо(" ПоказатьШаблоныйВКэше %+v ; \n", s.cache.(*cache).m)

	s.cache.(*cache).m.Range(func(key, value interface{}) bool {
		templatePath := key.(string)
		template := value.(*Template)
		Инфо("Путь шаблона: %s, Содержимое: %+s \n\n passedBlocks %+s\n", templatePath, template.processedBlocks, template.passedBlocks)
		// if "/index.html" == templatePath {

		именаБлоков := []string{}
		for имяБлока, _ := range template.processedBlocks {
			именаБлоков = append(именаБлоков, имяБлока)
			// Инфо("имяБлока %+v  %+v \n", имяБлока, телоБлока)
			// Инфо(" template.set %+v \n", template.set)
		}
		именаВставляемыхБлоков := []string{}
		for имяВставляемогоБлока, _ := range template.passedBlocks {
			именаВставляемыхБлоков = append(именаВставляемыхБлоков, имяВставляемогоБлока)
			// Инфо("именаВставляемыхБлоков %+v %+v \n", имяВставляемогоБлока, телоВставляемогоБлока)
		}

		Инфо("Файл:  %+v \n Блоки: \n %+v \n Вставляемые Блоки : \n %+v \n", templatePath, strings.Join(именаБлоков, ";\n"), strings.Join(именаВставляемыхБлоков, ";\n"))

		// }

		return true
	})
	// t := s.cache.Get("/контент/админ/формы/формаНовогоОбработчика.html")
	// Инфо(" %+v \n", t)
}

// Parse parses `contents` as if it were located at `templatePath`, but won't put the result into the cache.
// Any referenced template (e.g. via `extends` or `import` statements) will be tried to be loaded from the cache.
// If a referenced template has to be loaded and parsed, it will also not be put into the cache after parsing.
// Parse анализирует "содержимое" так, как если бы оно было расположено в "templatePath", но не помещает результат в кэш.
// Будет предпринята попытка загрузить из кэша любой шаблон, на который ссылаются (например, с помощью инструкций "extends" или "import").
// Если необходимо загрузить и проанализировать шаблон, на который ссылается ссылка, он также не будет помещен в кэш после синтаксического анализа.
func (s *Set) Parse(templatePath, contents string) (template *Template, err error) {
	templatePath = filepath.ToSlash(templatePath)
	switch path.Base(templatePath) {
	case ".", "/":
		return nil, errors.New("template path has no base name")
	}
	// make sure it's absolute and clean it
	templatePath = path.Join("/", templatePath)
	return s.parse(templatePath, contents, true)
}

func (s *Set) Парсинг(имяШаблона string, текстШаблона string, кэшировать bool) (t *Template, err error) {
	return s.parse(имяШаблона, текстШаблона, кэшировать)
}

// AddGlobal adds a global variable into the Set,
// overriding any value previously set under the specified key.
// It returns the Set it was called on to allow for method chaining.
// // Add Global добавляет глобальную переменную в Set,
// переопределяющую любое значение, ранее установленное в соответствии с указанным ключом.
// Она возвращает Set, для которого она была вызвана, чтобы разрешить объединение методов в цепочку.
func (s *Set) AddGlobal(key string, i interface{}) *Set {
	s.gmx.Lock()
	defer s.gmx.Unlock()
	s.globals[key] = reflect.ValueOf(i)
	return s
}
func (s *Set) ДобавитьГлобально(key string, i interface{}) *Set {
	s.gmx.Lock()
	defer s.gmx.Unlock()
	s.globals[key] = reflect.ValueOf(i)
	return s
}

// LookupGlobal returns the global variable previously set under the specified key.
// It returns the nil interface and false if no variable exists under that key.
// // Функция Lookup Global возвращает глобальную переменную, ранее установленную в соответствии с указанным ключом.
// Она возвращает интерфейс nil и значение false, если под этим ключом не существует переменной.
func (s *Set) LookupGlobal(key string) (val interface{}, found bool) {
	s.gmx.RLock()
	defer s.gmx.RUnlock()
	val, found = s.globals[key]
	return
}

// AddGlobalFunc adds a global function into the Set,
// overriding any function previously set under the specified key.
// It returns the Set it was called on to allow for method chaining.
func (s *Set) AddGlobalFunc(key string, fn Func) *Set {
	return s.AddGlobal(key, fn)
}

// // ДобавитьФункцию добавляет глобальную функцию в набор,
// переопределяя любую функцию, ранее установленную с помощью указанного ключа.
// Она возвращает набор, для которого она была вызвана, чтобы разрешить объединение методов в цепочку. вызывает под капотом s.ДобавитьГлобально(key, fn)
func (s *Set) ДобавитьФункцию(key string, fn Func) *Set {
	return s.ДобавитьГлобально(key, fn)
}

func (s *Set) ДобавитьФункции(НаборФункций КартаФункций) *Set {
	for key, fn := range НаборФункций {
		s.ДобавитьГлобально(key, fn)
	}
	return s
}
